/*
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 *
 * Largely taken from Cheese:
 *   Copyright (C) 2007,2008 Jaap Haitsma <jaap@haitsma.org>
 *   Copyright (C) 2007,2008 daniel g. siegel <dgsiegel@gmail.com>
 *   Copyright (C) 2008 Ryan Zeigler <zeiglerr@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdbool.h>
#include <libhal.h>
#include <glib.h>
#include <gst/gst.h>

#include "devices.h"

static GalaxiumWebcamDevice *libgalaxium_hal_find_devices (int *numdevices)
{
	int i;
	int num_udis;
	char **udis;
	GalaxiumWebcamDevice *devices;
	
	DBusError error;
	LibHalContext *hal_ctx;
	
	dbus_error_init (&error);
	hal_ctx = libhal_ctx_new ();	
	
	if (hal_ctx == NULL) 
	{
		g_error ("error: libhal_ctx_new");
		dbus_error_free (&error);
		*numdevices = 0;
		return NULL;
	}
	
	if (!libhal_ctx_set_dbus_connection (hal_ctx, dbus_bus_get (DBUS_BUS_SYSTEM, &error))) 
	{
		g_error ("error: libhal_ctx_set_dbus_connection: %s: %s", error.name, error.message);
		dbus_error_free (&error);
		*numdevices = 0;
		return NULL;
	}
	
	if (!libhal_ctx_init (hal_ctx, &error)) 
	{
		if (dbus_error_is_set(&error)) 
		{
			g_error ("error: libhal_ctx_init: %s: %s\n", error.name, error.message);
			dbus_error_free (&error);
		}
		
		g_warning ("Could not initialise connection to hald.\n"
				"Normally this means the HAL daemon (hald) is not running or not ready");
		
		*numdevices = 0;
		return NULL;
	}
	
	udis = libhal_find_device_by_capability (hal_ctx, "video4linux", &num_udis, &error);
	
	if (dbus_error_is_set (&error)) 
	{
		g_error ("error: %s: %s\n", error.name, error.message);
		dbus_error_free (&error);
		*numdevices = 0;
		return NULL;
	}
	
	if (num_udis <= 0)
	{
		*numdevices = 0;
		return NULL;
	}
	
	*numdevices = num_udis;
	
	devices = g_new0 (GalaxiumWebcamDevice, num_udis);
	for (i = 0; i < *numdevices; i++)
	{
		devices[i].num_video_formats = 0;
		devices[i].video_formats = NULL;

		devices[i].hal_udi = g_strdup (udis[i]);
		
		char *device = libhal_device_get_property_string (hal_ctx, udis[i], "video4linux.device", &error);
		
		if (dbus_error_is_set (&error)) 
		{
			g_error ("error: %s: %s\n", error.name, error.message);
			dbus_error_free (&error);
			*numdevices = 0;
			return NULL;
		}
		
		devices[i].video_device = g_strdup (device);
		libhal_free_string (device);
	}
	
	libhal_free_string_array (udis);
	
	return devices;
}

static void galaxium_get_webcam_framerates (GalaxiumVideoFormat *video_format, GstStructure *structure)
{
	const GValue *framerates;
	int i, j;
	
	framerates = gst_structure_get_value (structure, "framerate");
	
	if (GST_VALUE_HOLDS_FRACTION (framerates))
	{
		video_format->num_framerates = 1;
		video_format->framerates = g_new0 (GalaxiumFramerate, video_format->num_framerates);
		video_format->framerates[0].numerator = gst_value_get_fraction_numerator (framerates);
		video_format->framerates[0].denominator = gst_value_get_fraction_denominator (framerates);    
	}
	else if (GST_VALUE_HOLDS_LIST (framerates))
	{
		video_format->num_framerates = gst_value_list_get_size (framerates);
		video_format->framerates = g_new0 (GalaxiumFramerate, video_format->num_framerates);
		
		for (i = 0; i < video_format->num_framerates; i++)
		{
			const GValue *value;
			value = gst_value_list_get_value (framerates, i);
			video_format->framerates[i].numerator = gst_value_get_fraction_numerator (value);
			video_format->framerates[i].denominator = gst_value_get_fraction_denominator (value);
		}
	}
	else if (GST_VALUE_HOLDS_FRACTION_RANGE (framerates))
	{
		int numerator_min, denominator_min, numerator_max, denominator_max;
		const GValue *fraction_range_min;
		const GValue *fraction_range_max;
		
		fraction_range_min = gst_value_get_fraction_range_min (framerates);
		numerator_min = gst_value_get_fraction_numerator (fraction_range_min);
		denominator_min = gst_value_get_fraction_denominator (fraction_range_min);
		
		fraction_range_max = gst_value_get_fraction_range_max (framerates);
		numerator_max = gst_value_get_fraction_numerator (fraction_range_max);
		denominator_max = gst_value_get_fraction_denominator (fraction_range_max);
		//g_print ("FractionRange: %d/%d - %d/%d\n", numerator_min, denominator_min, numerator_max, denominator_max);
		
		video_format->num_framerates = (numerator_max - numerator_min + 1) * (denominator_max - denominator_min + 1);
		video_format->framerates = g_new0 (GalaxiumFramerate, video_format->num_framerates);
		
		int k = 0;
		
		for (i = numerator_min; i <= numerator_max; i++)
		{
			for (j = denominator_min; j <= denominator_max; j++)
			{
				video_format->framerates[k].numerator = i;
				video_format->framerates[k].denominator = j;
				k++;
			}
		}
	}
	else
	{
		g_critical ("GValue type %s, cannot be handled for framerates", G_VALUE_TYPE_NAME (framerates));
	}
}

static void galaxium_get_webcam_video_formats (GalaxiumWebcamDevice *device, GstCaps *caps)
{
	int i;
	int num_structures;
	GArray *formats = g_array_new (FALSE, FALSE, sizeof (GalaxiumVideoFormat));
	
  num_structures = gst_caps_get_size (caps);
  for (i = 0; i < num_structures; i++)
  {
    GstStructure *structure;
    const GValue *width, *height;
    structure = gst_caps_get_structure (caps, i);

    /* only interested in raw formats; we don't want to end up using image/jpeg
     * (or whatever else the cam may produce) since we won't be able to link
     * that to ffmpegcolorspace or the effect plugins, which makes it rather
     * useless (although we could plug a decoder of course) */
     
    if (!gst_structure_has_name (structure, "video/x-raw-yuv") &&
        !gst_structure_has_name (structure, "video/x-raw-rgb"))
    {
      continue;
    }

    width = gst_structure_get_value (structure, "width");
    height = gst_structure_get_value (structure, "height");

    if (G_VALUE_HOLDS_INT (width))
    {
      GalaxiumVideoFormat video_format;

      video_format.mimetype = g_strdup (gst_structure_get_name (structure));
      gst_structure_get_int (structure, "width", &(video_format.width));
      gst_structure_get_int (structure, "height", &(video_format.height));
      galaxium_get_webcam_framerates (&video_format, structure);

      g_array_append_val (formats, video_format);
      device->num_video_formats++;
    }
    else if (GST_VALUE_HOLDS_INT_RANGE (width))
    {
      int min_width, max_width, min_height, max_height;
      int cur_width, cur_height;

      min_width = gst_value_get_int_range_min (width);
      max_width = gst_value_get_int_range_max (width);
      min_height = gst_value_get_int_range_min (height);
      max_height = gst_value_get_int_range_max (height);

      cur_width = min_width;
      cur_height = min_height;
      while (cur_width < max_width && cur_height < max_height)
      {
        GalaxiumVideoFormat video_format;

        video_format.mimetype = g_strdup (gst_structure_get_name (structure));
        video_format.width = cur_width;
        video_format.height = cur_height;
        galaxium_get_webcam_framerates (&video_format, structure);
        g_array_append_val (formats, video_format);
        device->num_video_formats++;
        cur_width *= 2;
        cur_height *= 2;
      }

      cur_width = max_width;
      cur_height = max_height;
      while (cur_width > min_width && cur_height > min_height)
      {
        GalaxiumVideoFormat video_format;

        video_format.mimetype = g_strdup (gst_structure_get_name (structure));
        video_format.width = cur_width;
        video_format.height = cur_height;
        galaxium_get_webcam_framerates (&video_format, structure);
        g_array_append_val (formats, video_format);
        device->num_video_formats++;
        cur_width /= 2;
        cur_height /= 2;
      }
    }
    else
    {
      g_critical ("GValue type %s, cannot be handled for resolution width", G_VALUE_TYPE_NAME (width));
    }
  }
	
	device->video_formats = (GalaxiumVideoFormat *)g_array_free (formats, FALSE);
}

static void libgalaxium_get_webcam_device_data (GalaxiumWebcamDevice *device)
{
	char *pipeline_desc;
	GstElement *pipeline;
	GError *err;
	GstStateChangeReturn ret;
	GstMessage *msg;
	GstBus *bus;
	gboolean pipeline_works = FALSE;
	int i;

	static const char* GSTREAMER_VIDEO_SOURCES[] = 
	{
		"v4l2src",
		"v4lsrc"
	};
	
	i = 0;
	while (!pipeline_works && (i < G_N_ELEMENTS (GSTREAMER_VIDEO_SOURCES)))
	{
		pipeline_desc = g_strdup_printf ("%s name=source device=%s ! fakesink",
										GSTREAMER_VIDEO_SOURCES[i],
										device->video_device);
		
		err = NULL;
		pipeline = gst_parse_launch (pipeline_desc, &err);
		
		if ((pipeline != NULL) && (err == NULL))
		{
			/* Start the pipeline and wait for max. 10 seconds for it to start up */
			gst_element_set_state (pipeline, GST_STATE_PLAYING);
			ret = gst_element_get_state (pipeline, NULL, NULL, 10 * GST_SECOND);
			
			/* Check if any error messages were posted on the bus */
			bus = gst_element_get_bus (pipeline);
			msg = gst_bus_poll (bus, GST_MESSAGE_ERROR, 0);
			gst_object_unref (bus);
 			
			if ((msg == NULL) && (ret == GST_STATE_CHANGE_SUCCESS))
			{
				GstElement *src;
				char *name;
				GstPad* pad;
				GstCaps *caps;
				
				pipeline_works = TRUE;
				gst_element_set_state (pipeline, GST_STATE_PAUSED);
				
				device->gstreamer_src = g_strdup (GSTREAMER_VIDEO_SOURCES[i]);
				src = gst_bin_get_by_name (GST_BIN (pipeline), "source");
				
				g_object_get (G_OBJECT (src), "device-name", &name, NULL);
				
				if (name == NULL)
					name = "Unknown";
				
				device->name = g_strdup (name);
				
				pad = gst_element_get_pad (src, "src");
				caps = gst_pad_get_caps (pad);
				gst_object_unref (pad);
				galaxium_get_webcam_video_formats (device, caps);
				gst_caps_unref (caps);
			}
			
			gst_element_set_state (pipeline, GST_STATE_NULL);
 			gst_object_unref (pipeline);
		}
		
		if (err)
			g_error_free (err);
		
		g_free (pipeline_desc);
		i++;
	}
}

GalaxiumWebcamDevice *libgalaxium_find_webcams (int *numdevices)
{
	int num_hal_devices, i;
	GalaxiumWebcamDevice *hal_devices = libgalaxium_hal_find_devices (&num_hal_devices);
	
	for (i = 0; i < num_hal_devices; i++)
		libgalaxium_get_webcam_device_data (&hal_devices[i]);
	
	*numdevices = num_hal_devices;
	return hal_devices;
}
